Temukan kekuatan dari helper JavaScript Iterator `scan` yang baru. Pelajari bagaimana ia merevolusi pemrosesan aliran, manajemen state, dan agregasi data melampaui `reduce`.
Iterator JavaScript `scan`: Tautan yang Hilang untuk Pemrosesan Aliran Akumulatif
Dalam lanskap pengembangan web modern yang terus berkembang, data adalah raja. Kita terus-menerus berurusan dengan aliran informasi: peristiwa pengguna, respons API real-time, kumpulan data besar, dan banyak lagi. Memproses data ini secara efisien dan deklaratif adalah tantangan utama. Selama bertahun-tahun, pengembang JavaScript telah mengandalkan metode Array.prototype.reduce yang kuat untuk menyaring sebuah array menjadi satu nilai tunggal. Tapi bagaimana jika Anda perlu melihat perjalanannya, bukan hanya tujuannya? Bagaimana jika Anda perlu mengamati setiap langkah perantara dari sebuah akumulasi?
Di sinilah sebuah alat baru yang kuat masuk ke panggung: helper Iterator scan. Sebagai bagian dari proposal TC39 Iterator Helpers, yang saat ini berada di Stage 3, scan siap merevolusi cara kita menangani data sekuensial dan berbasis aliran di JavaScript. Ini adalah pasangan fungsional dan elegan dari reduce yang menyediakan riwayat lengkap dari suatu operasi.
Panduan komprehensif ini akan membawa Anda menyelam lebih dalam ke dalam metode scan. Kita akan menjelajahi masalah yang dipecahkannya, sintaksnya, kasus penggunaan yang kuat mulai dari total berjalan sederhana hingga manajemen state yang kompleks, dan bagaimana ia cocok dalam ekosistem JavaScript modern yang hemat memori.
Tantangan yang Lazim: Keterbatasan `reduce`
Untuk benar-benar menghargai apa yang dibawa oleh scan, mari kita kunjungi kembali skenario umum. Bayangkan Anda memiliki aliran transaksi keuangan dan Anda perlu menghitung saldo berjalan setelah setiap transaksi. Datanya mungkin terlihat seperti ini:
const transactions = [100, -20, 50, -10, 75]; // Setoran dan penarikan
Jika Anda hanya menginginkan saldo akhir, Array.prototype.reduce adalah alat yang sempurna:
const finalBalance = transactions.reduce((balance, transaction) => balance + transaction, 0);
console.log(finalBalance); // Output: 195
Ini ringkas dan efektif. Tapi bagaimana jika Anda perlu memplot saldo akun dari waktu ke waktu pada sebuah grafik? Anda memerlukan saldo setelah setiap transaksi: [100, 80, 130, 120, 195]. Metode reduce menyembunyikan langkah-langkah perantara ini dari kita; ia hanya menyediakan hasil akhir.
Jadi, bagaimana kita akan menyelesaikannya secara tradisional? Kita kemungkinan akan kembali ke loop manual dengan variabel state eksternal:
const transactions = [100, -20, 50, -10, 75];
const runningBalances = [];
let currentBalance = 0;
for (const transaction of transactions) {
currentBalance += transaction;
runningBalances.push(currentBalance);
}
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Ini berhasil, tetapi memiliki beberapa kelemahan:
- Gaya Imperatif: Ini kurang deklaratif. Kita secara manual mengelola state (
currentBalance) dan pengumpulan hasil (runningBalances). - Stateful dan Bertele-tele: Ini memerlukan pengelolaan variabel yang dapat diubah di luar loop, yang dapat meningkatkan beban kognitif dan potensi bug dalam skenario yang lebih kompleks.
- Tidak Dapat Disusun (Not Composable): Ini bukan operasi yang bersih dan dapat dirangkai. Ini memutus alur perangkaian metode fungsional (seperti
map,filter, dll.).
Inilah masalah yang dirancang untuk dipecahkan oleh helper Iterator scan dengan keanggunan dan kekuatan.
Paradigma Baru: Proposal Iterator Helpers
Sebelum kita langsung membahas scan, penting untuk memahami konteks di mana ia berada. Proposal Iterator Helpers bertujuan untuk menjadikan iterator sebagai warga kelas satu dalam JavaScript untuk pemrosesan data. Iterator adalah konsep fundamental dalam JavaScript—mereka adalah mesin di balik loop for...of, sintaks spread (...), dan generator.
Proposal ini menambahkan serangkaian metode yang familiar dan mirip array langsung ke Iterator.prototype, termasuk:
map(mapperFn): Mengubah setiap item dalam iterator.filter(filterFn): Menghasilkan hanya item yang lolos tes.take(limit): Menghasilkan N item pertama.drop(limit): Melewatkan N item pertama.flatMap(mapperFn): Memetakan setiap item ke iterator dan meratakan hasilnya.reduce(reducer, initialValue): Mereduksi iterator menjadi satu nilai.- Dan, tentu saja,
scan(reducer, initialValue).
Manfaat utama di sini adalah evaluasi malas (lazy evaluation). Tidak seperti metode array, yang sering membuat array perantara baru di memori, iterator helper memproses item satu per satu, sesuai permintaan. Ini membuat mereka sangat efisien memori untuk menangani aliran data yang sangat besar atau bahkan tak terbatas.
Penyelaman Mendalam ke Metode `scan`
Metode scan secara konseptual mirip dengan reduce, tetapi alih-alih mengembalikan satu nilai akhir, ia mengembalikan iterator baru yang menghasilkan hasil dari fungsi reducer pada setiap langkah. Ini memungkinkan Anda melihat riwayat lengkap dari akumulasi.
Sintaks dan Parameter
Tanda tangan metode ini lugas dan akan terasa akrab bagi siapa saja yang telah menggunakan reduce.
iterator.scan(reducer [, initialValue])
reducer(accumulator, element, index): Sebuah fungsi yang dipanggil untuk setiap elemen dalam iterator. Ia menerima:accumulator: Nilai yang dikembalikan oleh pemanggilan sebelumnya dari reducer, atauinitialValuejika disediakan.element: Elemen saat ini yang sedang diproses dari iterator sumber.index: Indeks dari elemen saat ini.
accumulatoruntuk panggilan berikutnya dan juga merupakan nilai yang dihasilkan olehscan.initialValue(opsional): Nilai awal untuk digunakan sebagaiaccumulatorpertama. Jika tidak disediakan, elemen pertama dari iterator digunakan sebagai nilai awal, dan iterasi dimulai dari elemen kedua.
Cara Kerjanya: Langkah-demi-Langkah
Mari kita telusuri contoh saldo berjalan kita untuk melihat scan beraksi. Ingat, scan beroperasi pada iterator, jadi pertama-tama, kita perlu mendapatkan iterator dari array kita.
const transactions = [100, -20, 50, -10, 75];
const initialBalance = 0;
// 1. Dapatkan iterator dari array
const transactionIterator = transactions.values();
// 2. Terapkan metode scan
const runningBalanceIterator = transactionIterator.scan(
(balance, transaction) => balance + transaction,
initialBalance
);
// 3. Hasilnya adalah iterator baru. Kita dapat mengubahnya menjadi array untuk melihat hasilnya.
const runningBalances = [...runningBalanceIterator];
console.log(runningBalances); // Output: [100, 80, 130, 120, 195]
Inilah yang terjadi di balik layar:
scandipanggil dengan reducer(a, b) => a + bdaninitialValuesebesar0.- Iterasi 1: Reducer dipanggil dengan
accumulator = 0(nilai awal) danelement = 100. Ia mengembalikan100.scanmenghasilkan100. - Iterasi 2: Reducer dipanggil dengan
accumulator = 100(hasil sebelumnya) danelement = -20. Ia mengembalikan80.scanmenghasilkan80. - Iterasi 3: Reducer dipanggil dengan
accumulator = 80danelement = 50. Ia mengembalikan130.scanmenghasilkan130. - Iterasi 4: Reducer dipanggil dengan
accumulator = 130danelement = -10. Ia mengembalikan120.scanmenghasilkan120. - Iterasi 5: Reducer dipanggil dengan
accumulator = 120danelement = 75. Ia mengembalikan195.scanmenghasilkan195.
Hasilnya adalah cara yang bersih, deklaratif, dan dapat disusun untuk mencapai apa yang kita butuhkan, tanpa loop manual atau manajemen state eksternal.
Contoh Praktis dan Kasus Penggunaan Global
Kekuatan scan jauh melampaui total berjalan sederhana. Ini adalah primitif fundamental untuk pemrosesan aliran yang dapat diterapkan pada berbagai domain yang relevan bagi pengembang di seluruh dunia.
Contoh 1: Manajemen State dan Event Sourcing
Salah satu aplikasi scan yang paling kuat adalah dalam manajemen state, meniru pola yang ditemukan di pustaka seperti Redux. Bayangkan Anda memiliki aliran tindakan pengguna atau peristiwa aplikasi. Anda dapat menggunakan scan untuk memproses peristiwa ini dan menghasilkan state aplikasi Anda di setiap titik waktu.
Mari kita modelkan penghitung sederhana dengan tindakan increment, decrement, dan reset.
// Fungsi generator untuk mensimulasikan aliran tindakan
function* actionStream() {
yield { type: 'INCREMENT' };
yield { type: 'INCREMENT' };
yield { type: 'DECREMENT', payload: 2 };
yield { type: 'UNKNOWN_ACTION' }; // Harus diabaikan
yield { type: 'RESET' };
yield { type: 'INCREMENT', payload: 5 };
}
// State awal aplikasi kita
const initialState = { count: 0 };
// Fungsi reducer mendefinisikan bagaimana state berubah sebagai respons terhadap tindakan
function stateReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + (action.payload || 1) };
case 'DECREMENT':
return { ...state, count: state.count - (action.payload || 1) };
case 'RESET':
return { count: 0 };
default:
return state; // PENTING: Selalu kembalikan state saat ini untuk tindakan yang tidak ditangani
}
}
// Gunakan scan untuk membuat iterator dari riwayat state aplikasi
const stateHistoryIterator = actionStream().scan(stateReducer, initialState);
// Catat setiap perubahan state saat terjadi
for (const state of stateHistoryIterator) {
console.log(state);
}
/*
Output:
{ count: 1 }
{ count: 2 }
{ count: 0 }
{ count: 0 } // a.k.a state tidak berubah oleh UNKNOWN_ACTION
{ count: 0 } // setelah RESET
{ count: 5 }
*/
Ini sangat kuat. Kita telah secara deklaratif mendefinisikan bagaimana state kita berevolusi dan menggunakan scan untuk membuat riwayat state yang lengkap dan dapat diamati. Pola ini fundamental untuk time-travel debugging, logging, dan membangun aplikasi yang dapat diprediksi.
Contoh 2: Agregasi Data pada Aliran Besar
Bayangkan Anda sedang memproses file log besar atau aliran data dari sensor IoT yang terlalu besar untuk dimuat ke dalam memori. Iterator helper bersinar di sini. Mari kita gunakan scan untuk melacak nilai maksimum yang terlihat sejauh ini dalam aliran angka.
// Generator untuk mensimulasikan aliran pembacaan sensor yang sangat besar
function* getSensorReadings() {
yield 22.5;
yield 24.1;
yield 23.8;
yield 28.3; // Maks baru
yield 27.9;
yield 30.1; // Maks baru
// ... bisa menghasilkan jutaan lebih banyak
}
const readingsIterator = getSensorReadings();
// Gunakan scan untuk melacak pembacaan maksimum dari waktu ke waktu
const maxReadingHistory = readingsIterator.scan((maxSoFar, currentReading) => {
return Math.max(maxSoFar, currentReading);
});
// Kita tidak perlu memberikan initialValue di sini. `scan` akan menggunakan
// elemen pertama (22.5) sebagai maks awal dan mulai dari elemen kedua.
console.log([...maxReadingHistory]);
// Output: [ 24.1, 24.1, 28.3, 28.3, 30.1 ]
Tunggu, outputnya mungkin tampak sedikit aneh pada pandangan pertama. Karena kita tidak memberikan nilai awal, scan menggunakan item pertama (22.5) sebagai akumulator awal dan mulai menghasilkan dari hasil reduksi pertama. Untuk melihat riwayat termasuk nilai awal, kita dapat memberikannya secara eksplisit, misalnya dengan -Infinity.
const maxReadingHistoryWithInitial = getSensorReadings().scan(
(maxSoFar, currentReading) => Math.max(maxSoFar, currentReading),
-Infinity
);
console.log([...maxReadingHistoryWithInitial]);
// Output: [ 22.5, 24.1, 24.1, 28.3, 28.3, 30.1 ]
Ini menunjukkan efisiensi memori dari iterator. Kita dapat memproses aliran data yang secara teoretis tak terbatas dan mendapatkan maksimum berjalan pada setiap langkah tanpa pernah menyimpan lebih dari satu nilai dalam memori pada satu waktu.
Contoh 3: Merangkai dengan Helper Lain untuk Logika Kompleks
Kekuatan sebenarnya dari proposal Iterator Helpers terbuka saat Anda mulai merangkai metode bersama-sama. Mari kita bangun pipeline yang lebih kompleks. Bayangkan aliran peristiwa e-commerce. Kita ingin menghitung total pendapatan dari waktu ke waktu, tetapi hanya dari pesanan yang berhasil diselesaikan oleh pelanggan VIP.
function* getECommerceEvents() {
yield { type: 'PAGE_VIEW', user: 'guest' };
yield { type: 'ORDER_PLACED', user: 'user123', amount: 50, isVip: false };
yield { type: 'ORDER_COMPLETED', user: 'user456', amount: 120, isVip: true };
yield { type: 'ORDER_FAILED', user: 'user789', amount: 200, isVip: true };
yield { type: 'ORDER_COMPLETED', user: 'user101', amount: 75, isVip: true };
yield { type: 'PAGE_VIEW', user: 'user456' };
yield { type: 'ORDER_COMPLETED', user: 'user123', amount: 30, isVip: false }; // Bukan VIP
yield { type: 'ORDER_COMPLETED', user: 'user999', amount: 250, isVip: true };
}
const revenueHistory = getECommerceEvents()
// 1. Filter untuk peristiwa yang tepat
.filter(event => event.type === 'ORDER_COMPLETED' && event.isVip)
// 2. Map hanya ke jumlah pesanan
.map(event => event.amount)
// 3. Scan untuk mendapatkan total berjalan
.scan((total, amount) => total + amount, 0);
console.log([...revenueHistory]);
// Mari kita telusuri alur data:
// - Setelah filter: { amount: 120 }, { amount: 75 }, { amount: 250 }
// - Setelah map: 120, 75, 250
// - Setelah scan (nilai yang dihasilkan):
// - 0 + 120 = 120
// - 120 + 75 = 195
// - 195 + 250 = 445
// Output Akhir: [ 120, 195, 445 ]
Contoh ini adalah demonstrasi indah dari pemrograman deklaratif. Kode tersebut terbaca seperti deskripsi logika bisnis: filter untuk pesanan VIP yang selesai, ekstrak jumlahnya, lalu hitung total berjalan. Setiap langkah adalah bagian kecil yang dapat digunakan kembali dan diuji dari pipeline yang lebih besar dan efisien memori.
`scan()` vs. `reduce()`: Perbedaan yang Jelas
Sangat penting untuk memperkuat perbedaan antara dua metode yang kuat ini. Meskipun mereka berbagi fungsi reducer, tujuan dan output mereka secara fundamental berbeda.
reduce()adalah tentang peringkasan. Ia memproses seluruh urutan untuk menghasilkan satu nilai akhir. Perjalanannya tersembunyi.scan()adalah tentang transformasi dan observasi. Ia memproses suatu urutan dan menghasilkan urutan baru dengan panjang yang sama, menunjukkan keadaan terakumulasi di setiap langkah. Perjalanannya adalah hasilnya.
Berikut adalah perbandingan berdampingan:
| Fitur | iterator.reduce(reducer, initial) |
iterator.scan(reducer, initial) |
|---|---|---|
| Tujuan Utama | Untuk menyaring urutan menjadi satu nilai ringkasan tunggal. | Untuk mengamati nilai terakumulasi di setiap langkah urutan. |
| Nilai Kembalian | Satu nilai tunggal (Promise jika asinkron) dari hasil akumulasi akhir. | Iterator baru yang menghasilkan setiap hasil akumulasi perantara. |
| Analogi Umum | Menghitung saldo akhir rekening bank. | Menghasilkan laporan bank yang menunjukkan saldo setelah setiap transaksi. |
| Kasus Penggunaan | Menjumlahkan angka, menemukan maksimum, menggabungkan string. | Total berjalan, manajemen state, menghitung rata-rata bergerak, mengamati data historis. |
Perbandingan Kode
const numbers = [1, 2, 3, 4].values(); // Dapatkan iterator
// Reduce: Tujuannya
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // Output: 10
// Anda memerlukan iterator baru untuk operasi berikutnya
const numbers2 = [1, 2, 3, 4].values();
// Scan: Perjalanannya
const runningSum = numbers2.scan((acc, val) => acc + val, 0);
console.log([...runningSum]); // Output: [1, 3, 6, 10]
Cara Menggunakan Iterator Helpers Hari Ini
Pada saat penulisan ini, proposal Iterator Helpers berada di Stage 3 dalam proses TC39. Ini berarti proposal ini sangat dekat untuk difinalisasi dan dimasukkan dalam versi standar ECMAScript di masa depan. Meskipun mungkin belum tersedia di semua browser atau lingkungan Node.js secara native, Anda tidak perlu menunggu untuk mulai menggunakannya.
Anda dapat menggunakan fitur-fitur canggih ini hari ini melalui polyfill. Cara paling umum adalah dengan menggunakan pustaka core-js, yang merupakan polyfill komprehensif untuk fitur JavaScript modern.
Untuk menggunakannya, Anda biasanya akan menginstal core-js:
npm install core-js
Dan kemudian mengimpor polyfill proposal spesifik di titik masuk aplikasi Anda:
import 'core-js/proposals/iterator-helpers';
// Sekarang Anda bisa menggunakan .scan() dan helper lainnya!
const result = [1, 2, 3].values()
.map(x => x * 2)
.scan((a, b) => a + b, 0);
console.log([...result]); // [2, 6, 12]
Sebagai alternatif, jika Anda menggunakan transpiler seperti Babel, Anda dapat mengkonfigurasinya untuk menyertakan polyfill dan transformasi yang diperlukan untuk proposal Stage 3.
Kesimpulan: Alat Baru untuk Era Data yang Baru
Helper JavaScript Iterator scan lebih dari sekadar metode baru yang nyaman; ia mewakili pergeseran menuju cara yang lebih fungsional, deklaratif, dan hemat memori dalam menangani aliran data. Ia mengisi celah penting yang ditinggalkan oleh reduce, memungkinkan pengembang untuk tidak hanya sampai pada hasil akhir tetapi juga mengamati dan bertindak berdasarkan seluruh riwayat akumulasi.
Dengan merangkul scan dan proposal Iterator Helpers yang lebih luas, Anda dapat menulis kode yang:
- Lebih Deklaratif: Kode Anda akan lebih jelas mengungkapkan apa yang ingin Anda capai, daripada bagaimana Anda mencapainya dengan loop manual.
- Lebih Dapat Disusun: Rangkai operasi sederhana dan murni untuk membangun pipeline pemrosesan data kompleks yang mudah dibaca dan dipahami.
- Lebih Hemat Memori: Manfaatkan evaluasi malas untuk memproses kumpulan data besar atau tak terbatas tanpa membebani memori sistem Anda.
Seiring kita terus membangun aplikasi yang lebih intensif data dan reaktif, alat seperti scan akan menjadi sangat diperlukan. Ini adalah primitif yang kuat yang memungkinkan pola canggih seperti event sourcing dan pemrosesan aliran untuk diimplementasikan secara native, elegan, dan efisien. Mulailah menjelajahinya hari ini, dan Anda akan siap untuk masa depan penanganan data di JavaScript.